﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using System.Timers;
using KC.Business.Scheduler;
using KC.Business.Scheduler.AdhocPlugins;
using KC.Business.Scheduler.ContinuousPlugins;
using KC.Business.Scheduler.ScheduledPlugins;
using SQL = KC.DataAccess.Global.SQL;
using Log = KC.DataAccess.Global.Logging;

namespace KC.Presentation.Scheduler
{
	public partial class SchedulerService : ServiceBase
	{
		private Timer timer;
		private List<Task> Queue = new List<Task>();
		private List<Type> AdhocPluginTypes = new List<Type>();
		private List<Type> ScheduledPluginTypes = new List<Type>();
		private List<Type> ContinuousPluginTypes = new List<Type>();
		private List<ContinuousPluginBase> ContinuousPlugins = new List<ContinuousPluginBase>();
		private bool isTimerRunning = false;
		private string ConnStr = System.Configuration.ConfigurationManager.ConnectionStrings["SQL"].ConnectionString;

		public SchedulerService() { InitializeComponent(); }

		protected async override void OnStart(string[] args)
		{
			LoadPlugins();
			timer.Interval = 60000;
			timer.Elapsed += timer_Elapsed;
			timer.Start();

			// Start all non-running ContinuousPlugins
			DataTable dt = await SQL.ExecuteDataTable(ConnStr, "select * from ContinuousJobs");
			foreach (DataRow dr in dt.Rows) 
				foreach (Type t in ContinuousPluginTypes) {
					ContinuousPluginBase plugin = (ContinuousPluginBase)Activator.CreateInstance(t);
					if (plugin.JobType.ToLower() == dr["JobType"].ToString()) {
						ContinuousPlugins.Add(plugin);
						plugin.Init(dr, ConnStr);
						plugin.Run(); } }
		}

		protected override void OnStop()
		{
			timer.Stop();
			Queue.Clear();
			Parallel.ForEach(ContinuousPlugins, p => p.Dispose());
		}

		async void timer_Elapsed(object sender, ElapsedEventArgs e)
		{
			if (isTimerRunning) return;
			DateTime started = DateTime.Now;
			isTimerRunning = true;

			// Enqueue all overdue ScheduledPlugins
			List<Task> tasks = new List<Task>();
			DataTable dt = await SQL.ExecuteDataTable(ConnStr, "select * from jobschedule where datediff(minute, lastrun, getdate()) > interval");
			await SQL.ExecuteNonQuery(ConnStr, "update JobSchedule set LastRun=getdate() where id in (" + string.Join(",", dt.AsEnumerable().Select(r => (r.Field<int>("ID")).ToString()).ToArray()) + ")");
			foreach (DataRow dr in dt.Rows) {
				DataRow drJob = (await SQL.ExecuteDataTable(ConnStr, "insert into JobRuns (jobType, Submitted, Started, Ended, Details, Result) values ('" + dr["JobType"].ToString() + "', getdate(), getdate(), null, '', 'Running'); "+
					"select * from jobruns where ID=@@IDENTITY")).Rows[0];

				tasks.Add(new Task(async () => {
					foreach (Type type in ScheduledPluginTypes) {
						ScheduledPluginBase plugin = (ScheduledPluginBase)Activator.CreateInstance(type);
						try { await StartJob(drJob, plugin); }
						catch (Exception ex) {
							Log.LogError(ex, ConnStr);
							SQL.ExecuteNonQuery(ConnStr, "update JobRuns set Details='" + (plugin.Results ?? "").ToString().Replace("'", "''") + "', result='Error', ended=getdate() where ID=" + drJob["ID"].ToString()); }} }, TaskCreationOptions.PreferFairness)); }
			Queue.AddRange(tasks);

			// Enqueue all awaiting AdhocPlugins
			dt = await SQL.ExecuteDataTable(ConnStr, "select * from jobruns where started is null and ended is null and enabled=1");
			foreach (DataRow dr in dt.Rows)
				tasks.Add(new Task(async () => {
					foreach (Type type in ScheduledPluginTypes) {
						ScheduledPluginBase plugin = (ScheduledPluginBase)Activator.CreateInstance(type);
						try { await StartJob(dr, plugin); }
						catch (Exception ex) {
							Log.LogError(ex, ConnStr);
							SQL.ExecuteNonQuery(ConnStr, "update JobRuns set Details='" + (plugin.Results ?? "").ToString().Replace("'", "''") + "', result='Error', ended=getdate() where ID=" + dr["ID"].ToString()); }} }, TaskCreationOptions.PreferFairness));

			// Start all jobs
			Queue.AddRange(tasks);
			Parallel.ForEach(tasks, t => t.Start());
			Task.WaitAll(tasks.ToArray());

			// Return control to service
			TimeSpan took = DateTime.Now - started;
			if (took > TimeSpan.FromMinutes(5))
				await Log.LogError(new ApplicationException("Scheduler Service timer instance ran for more than 5 minutes"), ConnStr, null, took.ToString());
			isTimerRunning = false;
		}

		private async Task StartJob(DataRow drJob, ScheduledPluginBase plugin) {
			if (plugin != null && plugin.JobType == drJob["JobType"].ToString()) {
				plugin.Init(drJob, ConnStr);
				plugin.Run();
				plugin.Dispose();
				await SQL.ExecuteNonQuery(ConnStr, "update JobRuns set Details='" + (plugin.Results ?? "").ToString().Replace("'", "''") + "', result='Finished', ended=getdate() where ID=" + drJob["ID"].ToString()); }
		}

		private void LoadPlugins()
		{
			foreach (string dll in Directory.GetFiles("", "*.dll"))
				foreach (Type type in Assembly.LoadFile(dll).GetTypes())
					try {
						if (type.IsSubclassOf(typeof(AdhocPluginBase))) AdhocPluginTypes.Add(type);
						else if (type.IsSubclassOf(typeof(ContinuousPluginBase))) ContinuousPluginTypes.Add(type);
						else if (type.IsSubclassOf(typeof(ScheduledPluginBase))) ScheduledPluginTypes.Add(type); }
					catch (BadImageFormatException) { }
					catch (Exception ex) { Log.LogError(ex, ConnStr); }
		}
	}
}
